S02-02 面向对象-封装
[TOC]
封装
OOP 三大特性
Java 面向对象编程的三大核心特性:封装、继承、多态
三大特性是 OOP 的基石,封装为 “基础”(保护数据、隐藏细节),继承为 “手段”(复用代码、构建层次),多态为 “延伸”(灵活扩展、解耦设计),三者相辅相成,共同构建健壮、可扩展的面向对象系统。
- 封装(Encapsulation):OOP 的基础,“隐藏细节,暴露接口”
- 继承(Inheritance):OOP 的手段,“复用代码,构建层次”
- 多态(Polymorphism):OOP 的延伸,“一个接口,多种实现”
封装概述
封装(Encapsulation):是指将对象的内部状态(属性)和行为(方法)捆绑在一起,隐藏内部实现细节,仅通过公开的接口与外部交互,从而实现 “信息隐藏” 和 “数据保护”。
本质:信息隐藏 + 数据保护
封装的核心是 “隐藏实现,暴露接口”:
- 信息隐藏:对外隐藏对象的内部状态和实现细节(如属性的存储方式、方法的执行逻辑),外部无法直接操作;
- 数据保护:通过可控的接口(getter/setter)操作数据,在接口中添加校验逻辑,保证数据的合法性和一致性。
核心优势
封装的核心优势:
数据安全:
通过 setter 中的校验逻辑,杜绝非法数据(如年龄负数、姓名为空),保证对象状态的一致性。
降低耦合:
外部仅依赖公开接口,不依赖内部实现。例如:修改
Person类的age属性名为userAge,只需修改getAge()/setAge()的内部实现,外部调用处无需任何修改。代码可维护:
所有数据访问逻辑集中在 getter/setter 中,修改时只需改一处。例如:将年龄的校验规则从
0-150改为0-120,仅需修改setAge()方法。增强可扩展性:
可在接口中灵活添加额外逻辑,不影响外部调用:
java// 扩展:在setter中添加日志 public void setAge(int age) { if (age < 0 || age > 150) { throw new IllegalArgumentException("年龄必须在0-150之间!"); } // 新增日志逻辑,外部调用无感知 System.out.println("修改年龄:" + this.age + " → " + age); this.age = age; }提升代码复用性:
内部私有方法可被多个公共接口复用,避免代码冗余。例如:
calculateDiscount()可被createOrder()、refundOrder()等方法复用。
实现步骤
封装的实现步骤:
Java 中封装的实现依赖访问修饰符(主要是 private)和公共接口(getter/setter),核心步骤如下:
步骤 1:私有化成员变量(核心)
将类的成员变量用
private修饰,禁止外部类直接访问,这是封装的基础。步骤 2:提供公共的 getter/setter 方法
IDEA 快捷键:
Alt + Insert- getter 方法:用于获取成员变量的值,命名规则
getXxx()(布尔类型可简化为isXxx()); - setter 方法:用于修改成员变量的值,命名规则
setXxx(参数); - 方法用
public修饰,作为外部访问私有变量的唯一入口。
- getter 方法:用于获取成员变量的值,命名规则
步骤 3:在 setter 中添加数据校验(可选但推荐)
在 setter 方法中校验传入值的合法性,拒绝非法数据(如年龄 < 0、姓名为空),保证数据一致性。
步骤 4:在 getter 中添加权限判断(可选)
步骤 5:隐藏内部业务逻辑(进阶)
将内部辅助方法(如计算、校验的工具方法)私有化,仅暴露对外的核心业务方法。
快速入门
对比未封装 vs 封装:
未封装的 Person 类(问题暴露)
成员变量用
public修饰,外部可直接访问和修改,导致数据失控:java// 未封装的类:数据不安全,耦合度高 public class UnEncapsulatedPerson { // 公共成员变量:外部可直接修改 public String name; public int age; public static void main(String[] args) { UnEncapsulatedPerson person = new UnEncapsulatedPerson(); // 问题1:可随意设置非法数据(年龄为负数) person.age = -20; // 问题2:可设置空姓名 person.name = ""; // 问题3:后续若修改属性名(如age改为userAge),所有外部调用处都要改 System.out.println("姓名:" + person.name + ",年龄:" + person.age); // 姓名:,年龄:-20 } }封装后的 Person 类(最佳实践)
私有化成员变量,提供带校验的 getter/setter,隐藏内部逻辑:
java// 封装后的类:数据安全,可维护性高 public class EncapsulatedPerson { // 1. 私有化成员变量:外部无法直接访问 private String name; private int age; // 2. 提供 getter 方法:获取属性值 public String getName() { return this.name; }// 3. 提供 setter 方法:修改属性值,添加数据校验 public void setName(String name) { // 校验:姓名不能为空或空白字符串 if (name == null || name.trim().isEmpty()) { throw new IllegalArgumentException("姓名不能为空!"); } this.name = name; } public int getAge() { return this.age; } public void setAge(int age) { // 校验:年龄必须在0-150之间 if (age < 0 || age > 150) { throw new IllegalArgumentException("年龄必须在0-150之间!"); } this.age = age; } // 4. 隐藏内部业务逻辑:私有化辅助方法 private void validatePerson() { System.out.println("校验人员信息:" + this.name + "," + this.age); } // 5. 暴露公共业务接口:外部仅能调用该方法 public void showPersonInfo() { this.validatePerson(); // 内部调用私有方法 System.out.println("姓名:" + this.name + ",年龄:" + this.age); } // 测试封装效果 public static void main(String[] args) { EncapsulatedPerson person = new EncapsulatedPerson(); // 合法数据:正常设置 person.setName("张三"); person.setAge(25); person.showPersonInfo(); // 校验人员信息:张三,25 → 姓名:张三,年龄:25 // 非法数据:抛出异常,保证数据安全 try { person.setName(""); // 抛出:IllegalArgumentException: 姓名不能为空! } catch (IllegalArgumentException e) { System.out.println(e.getMessage()); } try { person.setAge(-5); // 抛出:IllegalArgumentException: 年龄必须在0-150之间! } catch (IllegalArgumentException e) { System.out.println(e.getMessage()); } } } 关键对比:封装前后的差异
维度 未封装(public 变量) 封装(private 变量 + getter/setter) 数据安全 外部可随意设置非法值 仅能通过校验后的 setter 设置合法值 代码耦合 外部直接依赖属性名,修改属性名需改所有调用处 外部依赖接口,修改属性名仅需改 getter/setter 逻辑扩展 无法添加额外逻辑(如日志、校验) 可在 getter/setter 中添加日志、缓存等 可维护性 低(校验逻辑分散在各处) 高(校验逻辑集中在 setter)
访问修饰符
封装的实现依赖 Java 的访问修饰符,通过不同的修饰符控制类 / 成员的可见范围,核心修饰符对比如下:
| 修饰符 | 本类 | 同包 | 不同包子类 | 所有类 | 封装场景中的作用 |
|---|---|---|---|---|---|
private | ✅ | ❌ | ❌ | ❌ | 私有化成员变量 / 内部方法,核心封装修饰符 |
default | ✅ | ✅ | ❌ | ❌ | 同包内可见,用于包内封装 |
protected | ✅ | ✅ | ✅ | ❌ | 子类可见,用于继承场景的封装 |
public | ✅ | ✅ | ✅ | ✅ | 暴露公共接口(getter/setter/ 业务方法) |
关键说明:
- 封装的核心是
private:90% 的封装场景都是将成员变量设为private,仅暴露public接口; default用于包内封装:仅同包内的类可访问,适合包内复用的工具方法;protected用于继承封装:允许子类访问,但禁止外部无关类访问;public仅暴露必要接口:避免将所有方法设为public,遵循 “最小权限原则”。
构造器结合 setter
虽然在类中设置了 getter/setter,但是在初始化对象时会调用构造器,默认情况下可以通过构造器绕过设置的 getter/setter 方法。此时就需要通过在构造器中调用 setter 方法确保初始化时也能进行数据验证。
public Person (String name, Int age) {
// 在构造器中调用 setter 方法
this.setName(name);
this.setAge(age);
}封装进阶场景
只读/只写属性
只读 / 只写属性:按需暴露接口
封装并非必须同时提供 getter 和 setter,可根据需求仅暴露部分接口:
- 只读属性:只提供 getter,不提供 setter(如用户 ID,创建后不可修改);
- 只写属性:只提供 setter,不提供 getter(如密码,仅能设置,不能获取)。
public class User {
// 只读属性:用户ID(创建后不可改)
private final String userId;
// 只写属性:密码(仅能设置,不能获取)
private String password;
// 构造方法初始化只读属性
public User(String userId) {
this.userId = userId;
}
// 只读:仅提供getter
public String getUserId() {
return this.userId;
}
// 只写:仅提供setter,且添加密码强度校验
public void setPassword(String password) {
if (password == null || password.length() < 6) {
throw new IllegalArgumentException("密码长度不能小于6位!");
}
this.password = password;
}
}隐藏复杂业务逻辑
隐藏复杂业务逻辑:
将复杂的内部逻辑私有化,仅暴露简单的公共接口,降低外部使用成本:
public class OrderService {
// 私有化内部方法:计算折扣
private double calculateDiscount(double amount, int vipLevel) {
if (vipLevel == 1) return amount * 0.9;
if (vipLevel == 2) return amount * 0.8;
return amount;
}
// 私有化内部方法:生成订单号
private String generateOrderId() {
return "ORDER_" + System.currentTimeMillis();
}
// 暴露公共接口:创建订单(外部仅需调用该方法)
public String createOrder(double amount, int vipLevel) {
double finalAmount = this.calculateDiscount(amount, vipLevel);
String orderId = this.generateOrderId();
System.out.println("创建订单:" + orderId + ",金额:" + finalAmount);
return orderId;
}
}
// 外部调用:无需关心折扣计算、订单号生成的细节
class TestOrder {
public static void main(String[] args) {
OrderService service = new OrderService();
service.createOrder(1000, 2); // 创建订单:ORDER_17362xxxx,金额:800.0
}
}不可变类(终极封装)
不可变类:终极封装
不可变类是封装的极致形式:对象创建后,属性不可修改,核心特点:
- 成员变量私有化 +
final修饰; - 无 setter 方法;
- 构造方法初始化所有成员;
- 类用
final修饰(禁止继承,避免子类修改逻辑)。
Java 中的 String、Integer 等包装类都是不可变类:
// 自定义不可变类:地址类
public final class Address {
// 私有 + final 成员变量
private final String province;
private final String city;
// 构造方法初始化所有成员
public Address(String province, String city) {
this.province = province;
this.city = city;
}
// 仅提供 getter,无 setter
public String getProvince() {
return province;
}
public String getCity() {
return city;
}
}
// 测试不可变类:属性无法修改
class TestAddress {
public static void main(String[] args) {
Address addr = new Address("广东省", "深圳市");
System.out.println(addr.getProvince()); // 广东省
// 无 setter,无法修改属性,保证对象不可变
}
}练习
需求:创建 Account 类,要求:
- 姓名:长度 2-4 位,否则默认"无名"
- 余额:必须>20,否则默认 0
- 密码:必须 6 位,否则默认"000000"
- 提供 setter/getter 方法和
showInfo()方法
package com.hspedu.encap;
public class Account {
// 私有化属性
private String name;
private double balance;
private String pwd;
// 构造器
public Account() {}
public Account(String name, double balance, String pwd) {
this.setName(name);
this.setBalance(balance);
this.setPwd(pwd);
}
// getter 方法
public String getName() {
return name;
}
public double getBalance() {
return balance;
}
public String getPwd() {
return pwd;
}
// setter 方法(含验证)
public void setName(String name) {
if (name.length() >= 2 && name.length() <= 4) {
this.name = name;
} else {
System.out.println("姓名要求(长度为2-4位),默认值无名");
this.name = "无名";
}
}
public void setBalance(double balance) {
if (balance > 20) {
this.balance = balance;
} else {
System.out.println("余额(必须>20),默认为0");
this.balance = 0;
}
}
public void setPwd(String pwd) {
if (pwd.length() == 6) {
this.pwd = pwd;
} else {
System.out.println("密码(必须是六位),默认密码为000000");
this.pwd = "000000";
}
}
// 显示账号信息
public void showInfo() {
// 可以添加权限校验
System.out.println("账号信息:name=" + name + " 余额=" + balance + " 密码=" + pwd);
}
}// 测试类
package com.hspedu.encap;
public class TestAccount {
public static void main(String[] args) {
Account account = new Account();
account.setName("jack");
account.setBalance(60);
account.setPwd("123456");
account.showInfo(); // 账号信息:name=jack 余额=60.0 密码=123456
}
}构造方法
概述
构造方法(Constructor,构造器):是类中满足以下特征的特殊方法:
- 方法名必须与类名完全一致(包括大小写,如
Person类的构造方法名必须是Person); - 没有返回值类型(连
void都不能声明); - 不能被
static、final、abstract、native、synchronized等修饰(可被public/private/protected访问修饰符修饰); - 创建对象时由 JVM 自动调用,而非手动调用(仅能通过
this()/super()在构造方法内部调用其他构造)。
本质和作用:
构造方法的核心价值是保证对象创建时的初始化完整性:
- 初始化对象的成员变量(避免属性处于 “未初始化” 的默认值状态,如
int默认 0、String默认null); - 执行对象创建时的必要逻辑(如连接数据库、初始化集合、校验参数合法性);
- 控制对象的创建方式(如私有构造方法实现单例模式,禁止外部创建对象)。
语法格式
[访问修饰符] 类名([参数列表]) [throws 异常类型列表] {
// 构造方法体:初始化属性、执行初始化逻辑
}语法注意事项:
无返回值:构造方法不能声明返回值类型(包括
void),以下写法是错误的:java// 错误:不能写void public void Person() {} // 错误:不能写返回值类型 public int Person() { return 1; }方法名必须与类名一致:大小写错误会被识别为普通方法,而非构造方法:
javapublic class Person { // 错误:方法名是person(小写),类名是Person(大写),这是普通方法 public person() {} }
快速入门
// 定义Person类
public class Person {
// 成员变量
private String name;
private int age;
// 无参构造方法(自定义)
public Person() {
// 初始化默认值
this.name = "未知";
this.age = 0;
System.out.println("无参构造方法被调用");
}
// 有参构造方法(自定义)
public Person(String name, int age) {
// 初始化传入的属性值
this.name = name;
this.age = age;
System.out.println("有参构造方法被调用");
}
// 普通方法(对比构造方法)
public void showInfo() {
System.out.println("姓名:" + name + ",年龄:" + age);
}
public static void main(String[] args) {
// 创建对象时自动调用对应构造方法
Person p1 = new Person(); // 调用无参构造 → 无参构造方法被调用
p1.showInfo(); // 输出:姓名:未知,年龄:0
Person p2 = new Person("张三", 20); // 调用有参构造 → 有参构造方法被调用
p2.showInfo(); // 输出:姓名:张三,年龄:20
}
}核心特性@
构造方法的核心特性:
触发时机:仅在 new 对象时触发
构造方法不能像普通方法一样通过
对象名.方法名()调用,只能在创建对象时由 JVM 自动执行:javapublic class Test { public static void main(String[] args) { Person p = new Person(); // 自动调用构造方法 // p.Person(); // 错误:构造方法不能手动调用 } }默认构造方法(隐式无参构造)
如果类中没有定义任何构造方法,JVM 会自动生成一个隐式的无参构造方法(默认构造):
访问修饰符与类的修饰符一致(类是
public,默认构造也是public;类是default,默认构造也是default);方法体为空,仅完成对象的默认初始化(成员变量赋默认值)。
示例:
javapublic class Person { private String name; private int age; // 未定义任何构造方法,JVM自动生成默认无参构造 // 等价于:public Person() {} public static void main(String[] args) { Person p = new Person(); // 调用默认无参构造 System.out.println(p.name); // null(默认值) System.out.println(p.age); // 0(默认值) } }默认构造的 “消失规则”
如果类中自定义了任意构造方法(无论有参 / 无参),JVM 不再自动生成默认无参构造:
javapublic class Person { private String name; private int age; // 自定义有参构造,默认无参构造消失 public Person(String name) { this.name = name; } public static void main(String[] args) { // Person p = new Person(); // 错误:找不到无参构造方法 Person p = new Person("张三"); // 正确:调用自定义有参构造 } }解决方案:若需要无参构造,需手动显式定义。
构造方法可重载(核心特性)
构造方法支持重载(与普通方法重载
规则一致):同一个类中,多个构造方法名相同(类名),参数列表不同(个数 / 类型 / 顺序)。重载的
目的是提供多种对象初始化方式(如无参初始化默认值、有参初始化指定值):javapublic class Person { private String name; private int age; private String gender; // 重载1:无参构造(初始化默认值) public Person() { this.name = "未知"; this.age = 0; this.gender = "未知"; } // 重载2:单参数构造(仅初始化姓名) public Person(String name) { this.name = name; this.age = 0; this.gender = "未知"; } // 重载3:三参数构造(初始化所有属性) public Person(String name, int age, String gender) { this.name = name; this.age = age; this.gender = gender; } public static void main(String[] args) { Person p1 = new Person(); // 调用无参构造 Person p2 = new Person("李四"); // 调用单参数构造 Person p3 = new Person("王五", 25, "男"); // 调用三参数构造 } }构造方法不能被继承
子类不会继承父类的构造方法,只能通过
super()调用父类构造方法。构造方法不能被 static 修饰
static 修饰的方法属于类,而构造方法是创建对象时调用的,依赖对象实例,因此冲突:
java// 错误:构造方法不能被static修饰 public static Person() {}
调用规则
构造方法内部可通过 this() 调用本类其他构造方法,或通过 super() 调用父类构造方法,核心规则:
this()/super()必须是构造方法体的第一条语句;- 不能同时在一个构造方法中调用
this()和super()(因为第一条语句只能有一个); this()用于重载构造之间的复用,super()用于初始化父类属性。
this
this ():调用本类其他构造方法
目的是复用构造方法的初始化逻辑,减少代码冗余:
public class Person {
private String name;
private int age;
// 无参构造:调用有参构造,传入默认值
public Person() {
this("未知", 0); // 调用本类的Person(String, int)构造,必须在第一行
System.out.println("无参构造执行");
}
// 有参构造:核心初始化逻辑
public Person(String name, int age) {
this.name = name;
this.age = age;
System.out.println("有参构造执行");
}
public static void main(String[] args) {
Person p = new Person();
// 输出顺序:
// 有参构造执行
// 无参构造执行
}
}super
super ():调用父类构造方法:
子类构造方法必须调用父类构造方法(显式 / 隐式),确保父类属性先初始化:
隐式调用:若子类构造方法中未写
super(),JVM 会自动在第一行插入super()(调用父类无参构造);java// 父类 class Parent { private String parentName; // 父类无参构造 public Parent() { this.parentName = "父类默认名称"; System.out.println("父类无参构造执行"); } } // 子类 class Child extends Parent { private String childName; // 子类无参构造:隐式调用super()(父类无参构造) public Child() { // 隐式super(),等价于:super(); this.childName = "子类默认名称"; System.out.println("子类无参构造执行"); } } // 测试 public class Test { public static void main(String[] args) { Child c1 = new Child(); // 输出: // 父类无参构造执行 // 子类无参构造执行 } }显式调用:手动指定
super(参数)调用父类有参构造,必须在构造方法第一行。java// 父类 class Parent { private String parentName; // 父类有参构造 public Parent(String parentName) { this.parentName = parentName; System.out.println("父类有参构造执行"); } } // 子类 class Child extends Parent { private String childName; // 子类有参构造:显式调用父类有参构造 public Child(String parentName, String childName) { super(parentName); // 调用父类有参构造,必须在第一行 this.childName = childName; System.out.println("子类有参构造执行"); } } // 测试 public class Test { public static void main(String[] args) { Child c2 = new Child("父类自定义名称", "子类自定义名称"); // 输出: // 父类有参构造执行 // 子类有参构造执行 } }调用规则注意点:若父类没有无参构造(仅自定义有参构造),子类构造必须显式调用父类有参构造,否则编译报错:
java// 父类:仅自定义有参构造,无默认无参构造 class Parent { public Parent(String name) {} } // 子类:编译错误,因为隐式super()会调用父类无参构造(不存在) class Child extends Parent { public Child() { // 错误:Implicit super constructor Parent() is undefined. Must explicitly invoke another constructor } // 正确:显式调用父类有参构造 public Child(String name) { super(name); } }
对象初始化顺序
创建对象时,初始化顺序为:
静态变量/静态代码块(类加载时执行,仅一次) → 实例变量/构造代码块(每次创建对象执行) → 构造方法(每次创建对象执行)
核心结论:
静态相关(变量 / 代码块)在类加载时执行,仅执行一次;
实例相关(变量 / 构造代码块 / 构造方法)在每次创建对象时执行;
构造代码块先于构造方法执行(构造代码块是 “所有构造方法的公共逻辑”)。
public class InitOrder {
// 静态变量
private static String staticVar = "静态变量初始化";
// 实例变量
private String instanceVar = "实例变量初始化";
// 1. 静态代码块:类加载时执行,仅执行一次
static {
System.out.println(staticVar);
System.out.println("静态代码块执行");
}
// 2. 构造代码块:每次创建对象时执行,先于构造方法执行
{
System.out.println(instanceVar);
System.out.println("构造代码块执行");
}
// 3. 构造方法:每次创建对象时执行
public InitOrder() {
System.out.println("构造方法执行");
}
public static void main(String[] args) {
System.out.println("=====创建第一个对象=====");
InitOrder obj1 = new InitOrder();
System.out.println("=====创建第二个对象=====");
InitOrder obj2 = new InitOrder();
}
}输出结果:
静态变量初始化
静态代码块执行
=====创建第一个对象=====
实例变量初始化
构造代码块执行
构造方法执行
=====创建第二个对象=====
实例变量初始化
构造代码块执行
构造方法执行练习
给Person类添加两个构造器:
- 无参构造器:设置
age初始值为 18。 - 带
pName和pAge参数的构造器:初始化name和age。
public class ConstructorExercise {
public static void main(String[] args) {
Person p1 = new Person(); // 调用无参构造器
System.out.println("p1 的信息name=" + p1.name + " age=" + p1.age); // name=null,age=18
Person p2 = new Person("scott", 50); // 调用带参构造器
System.out.println("p2 的信息name=" + p2.name + " age=" + p2.age); // name=scott,age=50
}
}
class Person {
String name; // 默认值null
int age; // 默认值0
// 无参构造器:age初始值18
public Person() {
age = 18;
}
// 带参构造器:初始化name和age
public Person(String pName, int pAge) {
name = pName;
age = pAge;
}
}对象创建内存流程
对象创建内存流程:
- 加载 Person 类信息(Person.class),只会加载一次
- 在堆中分配空间(地址)
- 完成对象初始化
- 3.1 默认初始化:age=0,name=null
- 3.2 显式初始化:age=90,name=null
- 3.3 构造器的初始化:age=20,name=小倩
- 将对象在堆中的地址,返回给 p(p 是对象名,也可以理解成是对象的引用)
.jpg)
三层架构
在 Java 后端开发中,三层架构(Three-Tier Architecture) 是最经典、最基础,也是目前企业级开发中最普遍采用的软件设计规范。
如果说设计模式是“武功招式”,那么三层架构就是“内功心法”和“门派建制”。它的核心思想只有一个:高内聚,低耦合(职责分离)。把复杂的系统拆解成三个各司其职的部门。
哪三层
以下是这三个“部门”的详细介绍:
哪三层(The Three Layers):
在标准的 Java Web 应用(比如基于 Spring Boot 的应用)中,代码通常被划分为以下三层:
表现层 / 控制层 (Presentation / Controller Layer):
- 角色: 公司的“前台接待员”。
- 职责:
- 接收请求: 接收来自客户端(浏览器、App、小程序)的 HTTP 请求。
- 参数校验: 检查用户传来的数据格式对不对(比如邮箱格式、密码长度)。
- 请求转发: 自己不干具体的业务,而是把请求转交给下一层(Service 层)去处理。
- 返回结果: 把 Service 层处理完的结果,封装成标准格式(如 JSON 或 HTML 页面)返回给客户端。
- 常见技术/框架: Spring MVC、Servlet。
- 常见包名/注解:
controller包,@RestController,@Controller,@RequestMapping。
业务逻辑层 (Business Logic / Service Layer):
- 角色: 公司的“核心业务部门”或“经理”。
- 职责:
- 处理业务: 这里是代码的灵魂所在。所有的业务规则、计算公式、判断逻辑(例如:判断库存够不够、计算打折后的价格、生成订单号)都在这里完成。
- 事务控制: 保证一系列操作要么全部成功,要么全部失败回滚(例如转账时的扣款和加钱)。
- 组合调用: 它会调用下一层(DAO 层)的多个方法来完成一个复杂的业务。
- 常见技术/框架: Spring Core (IOC/AOP)。
- 常见包名/注解:
service包,@Service,@Transactional。
数据访问层 (Data Access Object / DAO / Mapper Layer / 持久层):
- 角色: 公司的“档案管理员”或“仓库保管员”。
- 职责:
- 操作数据库: 只负责一件事——和数据库打交道。执行增(Create)、删(Delete)、改(Update)、查(Retrieve)操作(简称 CRUD)。
- 纯粹性: 这一层绝对不能包含任何业务逻辑,它就像一个没有感情的工具人,Service 叫它查什么它就查什么。
- 常见技术/框架: MyBatis, MyBatis-Plus, Spring Data JPA, Hibernate, JDBC。
- 常见包名/注解:
dao,mapper,repository包,@Mapper,@Repository。
数据流转
数据是如何在三层之间流转的:
这三层不是相互独立的,它们像流水线一样协同工作。通常的数据流向是:
用户请求 ➡️ Controller 层 ➡️ Service 层 ➡️ DAO 层 ➡️ 数据库
数据库响应 ➡️ DAO 层 ➡️ Service 层 ➡️ Controller 层 ➡️ 返回给用户
为了在各层之间传递数据,Java 开发者定义了不同的数据载体(虽然新手经常混用,但在规范的大厂中分得很细):
- Entity / POJO: 与数据库表结构一一对应的实体类(通常在 DAO 层和 Service 层之间流转)。
- DTO (Data Transfer Object): 数据传输对象,用于 Service 层和 Controller 层之间传递聚合后的数据。
- VO (View Object): 视图对象,Controller 层返回给前端用于展示的数据。

三层架构优点
为什么要用三层架构(核心优势):
如果你把所有代码(接收请求、业务判断、写 SQL)都塞在一个类里(早期的 JSP 开发就是这么干的),代码也能跑,但为什么我们要费劲拆分呢?
| 优势 | 详细说明 |
|---|---|
| 解耦与易维护 | 如果数据库从 MySQL 换成 Oracle,只需修改 DAO 层代码,Controller 和 Service 层完全不用动。这叫“牵一发而不动全身”。 |
| 复用性强 | 一个 Service 方法(例如 getUserInfo)既可以被 Web 端网页的 Controller 调用,也可以被手机端 App 的 Controller 调用。 |
| 利于团队协作 | 前端人员和 Controller 联调,后端 A 写 Service,后端 B 写 DAO 和 SQL。大家定义好接口,可以并行开发。 |
| 易于测试 | 可以使用 Mock 工具单独测试 Service 层的业务逻辑,而不需要真的去连数据库或启动 Web 服务器。 |
代码结构示例
代码结构示例:
在实际的 IntelliJ IDEA 或 Eclipse 项目中,目录结构通常长这样:
com.example.project
├── controller // 1. 表现层 (处理 API 路由)
│ └── UserController.java
├── service // 2. 业务层 (接口)
│ ├── UserService.java
│ └── impl // 业务层实现类
│ └── UserServiceImpl.java
├── dao (或 mapper) // 3. 数据层 (操作数据库)
│ └── UserMapper.java
├── entity (或 model)// 4. 实体类 (对应数据库表)
│ └── User.java
├── utils // 5. 工具类
│ └── Format.java
└── ProjectApplication.java // 6. 启动类JavaBean
什么是 JavaBean
在 Java 开发中,JavaBean 是一个极其重要的概念,尤其是当我们刚刚聊完“三层架构”和数据在各层之间流转(如 POJO、DTO、VO)之后,理解 JavaBean 恰逢其时。
JavaBean:并不是一个特定的类,也不是什么高深的技术,它仅仅是一种“类设计规范”或“约定”。
只要你的普通 Java 类遵循了这套规范,我们就可以把它叫做一个 JavaBean。
四大核心规范
JavaBean 的四大核心规范 (The Rules):
要想成为一个合格的 JavaBean,你的类必须严格遵守以下四个条件:
所有的属性(成员变量)必须是
private的:- 目的: 保证数据的安全性(封装特性),防止外部直接通过
对象.属性的方式随意篡改数据。
- 目的: 保证数据的安全性(封装特性),防止外部直接通过
必须提供一个
public的无参构造方法:- 目的: 绝大多数 Java 框架(如 Spring、MyBatis、Hibernate)在底层都是通过反射机制 (Reflection) 来自动创建对象的。反射默认调用的就是无参构造方法。如果没有它,框架在尝试帮你自动实例化对象时就会直接报错。
必须提供
public的getter和setter方法:- 目的: 提供对外的、标准化的数据访问接口。
- 命名规范极其严格:
- 普通属性
name,方法必须叫getName()和setName()。 - 如果是
boolean类型的属性(如married),获取方法通常叫isMarried(),设置方法叫setMarried()。
- 普通属性
建议实现
java.io.Serializable接口 (序列化):- 目的: JavaBean 通常用来承载数据(比如从数据库查出来的用户信息)。这些数据经常需要跨网络传输给前端,或者保存到硬盘(缓存)中。实现序列化接口,能让这个对象变成二进制流在网络中安全穿梭。
标准 JavaBean 代码示例:
结合以上四点,一个标准的 JavaBean 应该是这样的(可以通过快捷键 Alt + Insert 自动生成无参构造、有参构造、Getter/Setter):
import java.io.Serializable;
// 1. 实现 Serializable 接口
public class UserBean implements Serializable {
// 强烈建议加上序列化版本号 (虽然不加也能跑,但加上更规范)
private static final long serialVersionUID = 1L;
// 2. 属性私有化 (private)
private String username;
private int age;
private boolean active;
// 3. 必须有公共的无参构造方法 (就算你不写,编译器默认也会送一个;但如果你写了有参构造,就必须手动把无参构造补上!)
public UserBean() {
}
// (可选) 为了自己用着方便,可以加一个有参构造
public UserBean(String username, int age, boolean active) {
this.username = username;
this.age = age;
this.active = active;
}
// 4. 提供标准的 Getter 和 Setter
public String getUsername() {
return username;
}
public void setUsername(String username) {
this.username = username;
}
public int getAge() {
return age;
}
public void setAge(int age) {
this.age = age;
}
// 注意 boolean 类型的 Getter 是 is 开头
public boolean isActive() {
return active;
}
public void setActive(boolean active) {
this.active = active;
}
}应用场景
为什么要制定这套规范(应用场景):
你可能会觉得:这也太啰嗦了吧?写几个变量非要搞出这么多代码!
这主要是为了迎合框架和组件化开发:
和数据表相对应:框架会根据数据库中的表,自动生成相对应的 JavaBean,它们的对应关系如下:

框架添加功能(自动化赋值): 比如你在前端网页的表单里填了用户名和密码点击提交。后端的 Spring MVC 框架会自动
new一个你的 JavaBean,然后通过反射调用setUsername()和setPassword(),把你填的数据自动塞进去。如果你的方法名不规范,框架就找不到该往哪塞数据了。
框架查询功能:
将从数据库中查询出来的结果封装成多个javabean对象,然后将多个javabean对象放到集合中,一起返回给页面,进行展示。

JSP/前端模板引擎:
早期的 JSP 技术中有
<jsp:useBean>标签,后来的 Thymeleaf 或 Vue 等前端技术,在读取后端传来的数据时,底层其实也是去找 JavaBean 的 getter 方法,而不是直接读变量。
JavaBean vs POJO
概念辨析:JavaBean vs POJO:
这两个词经常被混用,面试官也很喜欢问:
- POJO (Plain Old Java Object - 简单老式 Java 对象): 它是一个“泛指”。只要你是一个普通的 Java 类,没有继承什么乱七八糟的框架特有类(比如没继承
HttpServlet),你就是一个 POJO。 - JavaBean: 它是一个“特指”。它是严格遵守了上述四大规范的 POJO。
一句话总结:JavaBean 是一种要求更严格的 POJO。 在三层架构中,我们在层与层之间传递的 DTO、VO、Entity,绝大多数情况下都必须写成 JavaBean 的规范。
现代 Java 开发中的神器
现代 Java 开发中的“救星”:
手写这些 Getter/Setter 和无参构造确实非常枯燥,所以现代开发中我们有两件神器:
Lombok 插件:
只需要在类头上加一个
@Data注解,它就会在编译时自动帮你生成所有的 Getter/Setter、无参构造以及toString()等方法。源码非常干净。Java 14+ 的
Record类:如果你使用的是较新的 JDK,并且你的数据对象创建后就不再修改(只读),你可以直接使用
record关键字,一行代码搞定:javapublic record User(String username, int age) {}